6.7 Abstrakte Klassen und Methoden
 
Je weiter man nach »oben« in einer Klassenhierarchie geht, desto allgemeiner und unverbindlicher werden die Klassen. Die oberste Klasse ist meistens so allgemein, dass man sie sich gerade noch als Gerüst oder als Oberbegriff für alle abgeleiteten Klassen vorstellen kann. Denken Sie noch einmal an das Beispiel der Klassenhierarchie der Luftfahrzeuge. Ein Programm, das diese Hierarchie nutzt, wird Instanzen der Klassen Hubschrauber, Zeppelin und Starrflügler erstellen. Den aus diesen drei Klassen erstellten Objekten sind einige Verhaltensweisen und Eigenschaften gemein: Alle Objekte werden – unabhängig vom Typ – sowohl starten als auch fliegen und landen können, alle werden einen Kaufpreis haben, einen Hersteller usw.
Haben Sie dieses Kapitel von Anfang an aufmerksam gelesen, werden Sie die Schlussfolgerung ziehen, dass man die gemeinsamen Entitäten der verschiedenen Objekte sinnvollerweise in der Basisklasse Luftfahrzeug implementiert und an die Subklassen vererbt.
Vom Ansatz her ist diese Idee richtig, aber denken Sie einen Schritt weiter: Wie soll beispielsweise die Methode Starten in Luftfahrzeug implementiert werden? Ein Starrflügler wird anders starten als ein Hubschrauber und ein Hubschrauber wiederum anders als ein Zeppelin. Ein Flugzeug benötigt eine Startbahn mit einer Mindestlänge, um überhaupt abheben zu können, während für einen Hubschrauber bereits eine freie Fläche in einer Größe genügt, die gewährleistet, dass die Rotorspitzen keine Hindernisse streifen. Um einen Zeppelin zu starten, wird eine Mannschaft benötigt, die das Halteseil löst und das Luftschiff zum Starten freigibt.
Ein neues Problem wird plötzlich offensichtlich: Wie kann das Startverhalten in der Basisklasse implementiert werden, wenn es für die Objekte der abgeleiteten Typen Zeppelin, Hubschrauber und Starrflügler unterschiedlich beschrieben werden muss? Ein trivialer Lösungsansatz würde die Methode Starten in der Basisklasse definieren und den Startvorgang eines beliebigen Typs implementieren – zum Beispiel den eines Starrflüglers. Zugegeben, für die meisten real existierenden Flugobjekte würde damit die passende Implementierung geliefert. Die anderen beiden Typen würden diese Methode zwar erben, müssten aber darauf achten, sie mit new zu verdecken und neu zu implementieren.
Das kann es aber doch nicht sein, was wir wirklich wollen: Eine bestimmte Subklasse erbt die Funktionalität ihrer Basisklasse und kann sie uneingeschränkt nutzen, alle anderen Subklassen stehen in der Verantwortung, die geerbte Methode durch eine individuelle Implementierung zu ersetzen. Wird das in der Zeppelin-Klasse versäumt, rast ein Zeppelin beim Starten über die 2.000 m lange Startbahn bis zur Abhebegeschwindigkeit von 250 km/h ... sicherlich sehr zum Leidwesen der Bodenmannschaft.
6.7.1 Abstrakte Definitionen
 
Der Lösung haftet anscheinend der Nachteil an, dass sie in den abgeleiteten Klassen nicht auf die gleiche Weise behandelt werden kann. Sie ist damit fehlerträchtig und nicht gut. Jede abgeleitete Klasse sollte hinsichtlich der Behandlung einer Basisklassenoperation gleichwertig sein: Entweder müssen alle abgeleiteten Klassen die Starten-Methode neu implementieren oder keine. Nur so lassen sich potenzielle Fehler im Ansatz vermeiden.
Die Lösung der erkannten Problematik mag im ersten Moment verblüffen: Tatsächlich wird die Methode Starten in der Basisklasse nicht implementiert – sie bleibt einfach ohne Programmcode. In der objektorientierten Programmierung werden solche Methoden, die keinen Code enthalten, als abstrakte Methoden bezeichnet. Neben den das Verhalten eines Typs beschreibenden Methoden können auch Eigenschaften abstrakt definiert werden.
In C# werden abstrakte Methoden durch die Angabe des abstract-Modifizierers in der Methodensignatur gekennzeichnet, am Beispiel unserer Starten-Methode also:
| public abstract void Starten();
|
Abstrakte Methoden enthalten grundsätzlich keinen Code. Daher wird die Definition einer abstrakten Methode mit einem Semikolon hinter der Parameterliste abgeschlossen, die geschweiften Klammern des Anweisungsblocks entfallen.
Welchen Stellenwert nimmt aber eine Klasse ein, die eine Methode veröffentlicht, die keinerlei Verhalten aufweist? Die Antwort ist verblüffend, eine solche Klasse kann nicht instanziiert werden – sie rechtfertigt ihre Existenz einzig und allein dadurch, als Methodengeber für abgeleitete Klassen zu dienen. Damit wird das Prinzip der objektorientierten Programmierung, gemeinsame Verhaltensweisen auf eine höhere Ebene auszulagern, nahezu auf die Spitze getrieben.
Eine nicht instanziierbare Klasse, die mindestens ein durch abstract gekennzeichnetes Member enthält, ist ihrerseits selbst abstrakt und wird deshalb als abstrakte Klasse bezeichnet. Abstrakte Klassen machen nur dann Sinn, wenn sie abgeleitet werden. Syntaktisch wird dieses Verhalten in C# durch die Ergänzung des Modifikators abstract in der Klassensignatur beschrieben:
| public abstract class Luftfahrzeug {
|
| public abstract void Starten();
|
| ...
|
| }
|
Neben abstrakten Methoden darf eine abstrakte Klasse auch vollständig implementierte Methoden und Eigenschaften bereitstellen. So könnte die Klasse Luftfahrzeug beispielsweise den Hersteller des Luftfahrzeugs über eine Eigenschaftsmethode speichern und zurückgeben:
| public abstract class Luftfahrzeug {
|
| private string hersteller;
|
| // abstrakte Methode
|
| public abstract void Starten();
|
| // konkrete Eigenschaft
|
| public string Hersteller {
|
| get {return hersteller;}
|
| set {hersteller = value;}
|
| }
|
| }
|
Die Signierung einer Methode und infolgedessen auch der dazugehörigen Klasse mit dem Modifizierer abstract kommt einer Forderung gleich:
| Alle nicht abstrakten Ableitungen einer abstrakten Klasse müssen die abstrakten Methoden der Basisklasse überschreiben.
|
Wird in einer abgeleiteten Klasse das abstrakte Mitglied der Basisklasse nicht überschrieben, muss die Subklasse in jedem Fall ebenfalls abstract gekennzeichnet werden. Als Konsequenz dieser Aussagen bilden abstrakte Klassen das Gegenkonstrukt zu den Klassen, die mit sealed als nicht ableitbar gekennzeichnet sind. Daraus folgt auch, dass die Modifizierer sealed und abstract nicht nebeneinander verwendet werden dürfen.
Das folgende Codefragment beschreibt die Klasse Hubschrauber. In der Klassenimplementierung wird die abstrakte Methode Starten der Basisklasse überschrieben. Zur Kennzeichnung des Überschreibens einer abstrakten Basisklassenmethode dient der Modifizierer override in der überschreibenden Methode:
| class Hubschrauber : Luftfahrzeug {
|
| public override void Starten() {
|
| Console.WriteLine("Der Hubschrauber startet");
|
| }
|
| }
|
Eine Klasse, die eine abstrakt definierte Methode enthält, muss ihrerseits selbst abstrakt sein. Der Umkehrschluss ist allerdings nicht richtig, denn eine abstrakte Klasse ist nicht zwangsläufig dadurch gekennzeichnet, ein abstraktes Mitglied zu enthalten. Eine Klasse kann auch dann abstrakt sein, wenn keiner ihrer Member abstrakt ist. Auf diese Weise wird eine Klasse nicht instanziierbar und das Ableiten dieser Klasse erzwungen.
| abstract kann nur im Zusammenhang mit Instanzmembern benutzt werden. Statische Methoden können nicht abstrakt definiert werden, deshalb ist das gleichzeitige Auftreten von static und abstract in einer Methodensignatur nicht zulässig.
|
|